﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="With_WeakCollection.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// <summary>
//   Defines the WeakCollectionTests type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon
{
    using System;
    using System.Linq;
    using Collections;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Testing;

    // ReSharper disable RedundantAssignment
    [TestClass]
    public class With_WeakCollection
    {
        #region Fields

        protected WeakCollection<object> SystemUnderTest { get; set; }
        protected readonly object SystemUnderTestStrongReference1 = new object();
        protected readonly object SystemUnderTestStrongReference2 = new object();

        #endregion

        [TestInitialize]
        public virtual void Given()
        {
            SystemUnderTest = new WeakCollection<object>();
        }

        [TestClass]
        public class When_added_and_reference_is_alive : With_WeakCollection
        {
            [TestInitialize]
            public override void Given()
            {
                base.Given();
                
                SystemUnderTest.Add(SystemUnderTestStrongReference1);
            }

            [TestMethod]
            public void Should_increment_count()
            {
                Assert.AreEqual(1, SystemUnderTest.Count);
            }

            [TestMethod]
            public void Should_be_able_to_read_added_value()
            {
                Assert.AreEqual(SystemUnderTestStrongReference1, SystemUnderTest.FirstOrDefault());
            }
        }
        
        [TestClass]
        public class When_removed_and_reference_is_alive : With_WeakCollection
        {
            [TestInitialize]
            public override void Given()
            {
                base.Given();
                SystemUnderTest.Add(SystemUnderTestStrongReference1);
                SystemUnderTest.Remove(SystemUnderTestStrongReference1);
            }

            [TestMethod]
            public void Should_decrement_count()
            {
                Assert.AreEqual(0, SystemUnderTest.Count);
            }

            [TestMethod]
            public void Should_not_be_able_to_read_removed_value()
            {
                Assert.AreEqual(null, SystemUnderTest.FirstOrDefault());
            }
        }

        [TestClass]
        public class When_cleared : With_WeakCollection
        {
            [TestInitialize]
            public override void  Given()
            {
                base.Given();
                SystemUnderTest.Add(SystemUnderTestStrongReference1);
                SystemUnderTest.Add(SystemUnderTestStrongReference1);
                SystemUnderTest.Clear();
            }

            [TestMethod]
            public void Should_set_count_to_zero()
            {
                Assert.AreEqual(0, SystemUnderTest.Count);    
            }

            [TestMethod]
            public void Should_set_capacity_to_zero()
            {
                Assert.AreEqual(0, SystemUnderTest.Capacity);    
            }

            [TestMethod]
            public void Should_not_enumerate_any_items()
            {
                Assert.IsFalse(SystemUnderTest.Any());
            }
        }

        [TestClass]
        public class When_added_items_have_been_garbage_collected_whilst_strong_references_remain : With_WeakCollection
        {
            [TestInitialize]
            public override void Given()
            {
                base.Given();
                // add objct
                var weakObj1 = new object();
                var weakObj2 = new object();

                SystemUnderTest.Add(weakObj1);
                SystemUnderTest.Add(SystemUnderTestStrongReference1);
                SystemUnderTest.Add(weakObj2);

                // clear object
                weakObj1 = null;
                weakObj2 = null;
                ForceFullGarbageCollection();
            }

            [TestMethod]
            public void Should_not_update_count()
            {
                Assert.AreEqual(3, SystemUnderTest.Count);    
            }

            [TestMethod]
            public void Should_only_include_strong_references_when_enumerating()
            {
                // force an enumeration by using the predicated version of count()
                Assert.AreEqual(1, SystemUnderTest.Count(x => true));    
            }

            [TestMethod]
            public void Should_only_include_strong_reference_when_converting_to_a_list()
            {
                ListAssert.AreEqual(new[] { SystemUnderTestStrongReference1 }, SystemUnderTest.ToList());
            }
        }

        [TestClass]
        public class When_compacting_whilst_strong_references_remain : With_WeakCollection
        {
            [TestInitialize]
            public override void Given()
            {
                base.Given();
                
                var weakObj = new object();
                for (var i = 0; i < 8; i++)
                {
                    SystemUnderTest.Add(weakObj);
                }

                SystemUnderTest.Add(SystemUnderTestStrongReference1);

                // must keep the reference alive until we have finished adding items
                GC.KeepAlive(weakObj);
                weakObj = null;
                ForceFullGarbageCollection();

                // compact and verify
                SystemUnderTest.Compact();
            }

            [TestMethod]
            public void Should_not_shrink_capacity_below_4()
            {
                Assert.AreEqual(4, SystemUnderTest.Capacity);
            }

            [TestMethod]
            public void Should_update_count()
            {
                Assert.AreEqual(1, SystemUnderTest.Count);
            }
        }

        [TestClass]
        public class When_copying_to_array : With_WeakCollection
        {
            public object[] CopiedItems;
            public object NullObject = new object();
            public object WeakObject;
            public WeakReference WeakObjectReference;

            [TestInitialize]
            public override void Given()
            {
                base.Given();

                // setup
                WeakObject = new object();
                WeakObjectReference = new WeakReference(WeakObject);

                SystemUnderTest.Add(WeakObject);
                SystemUnderTest.Add(SystemUnderTestStrongReference1);
                SystemUnderTest.Add(WeakObject);
                SystemUnderTest.Add(SystemUnderTestStrongReference2);

                // GC obj1
                WeakObject = null;
                GC.Collect();

                // copy in seperate method to ensure JIT/Compiler optimizations don't ignore our collect
                VerifyTargetIsCollectedAndCopy();
            }

            private void VerifyTargetIsCollectedAndCopy()
            {
                // verify we collected the target
                Assert.IsNull(WeakObjectReference.Target);
                
                // copy the items
                CopiedItems = new object[SystemUnderTest.Count + 1];
                for (var i = 0; i < CopiedItems.Length; i++)
                {
                    CopiedItems[i] = NullObject;
                }
                SystemUnderTest.CopyTo(CopiedItems, 1);
            }

            [TestMethod]
            public void Should_start_copy_at_specified_index()
            {
                Assert.AreEqual(NullObject, CopiedItems[0]);
            }

            [TestMethod]
            public void Should_copy_strong_references_in_order_to_the_start_of_the_array()
            {
                Assert.AreEqual(SystemUnderTestStrongReference1, CopiedItems[1]);
                Assert.AreEqual(SystemUnderTestStrongReference2, CopiedItems[2]);
            }

            [TestMethod]
            public void Should_copy_collected_items_as_null_to_the_end_of_the_array()
            {
                Assert.IsNull(CopiedItems[3]);
                Assert.IsNull(CopiedItems[4]);
            }
        }
        
        [TestClass]
        public class When_about_to_expanding_capacity_whilst_collectable_items_exist : With_WeakCollection
        {
            public int CapacityRequiredToHoldOneItem;

            [TestInitialize]
            public override void Given()
            {
                base.Given();

                var weakObj = new object();
                
                // add up to capacity
                SystemUnderTest.Add(weakObj);
                
                CapacityRequiredToHoldOneItem = SystemUnderTest.Capacity;
                while (SystemUnderTest.Count < CapacityRequiredToHoldOneItem)
                {
                    SystemUnderTest.Add(weakObj);
                }

                // GC object
                weakObj = null;
                ForceFullGarbageCollection();

                // adding here will cause a compact, which should release all the weak references
                SystemUnderTest.Add(SystemUnderTestStrongReference1);
                
            }

            [TestMethod]
            public void Should_compact()
            {
                Assert.AreEqual(CapacityRequiredToHoldOneItem, SystemUnderTest.Capacity);
            }

            [TestMethod]
            public void Should_update_count()
            {
                Assert.AreEqual(1, SystemUnderTest.Count);
            }
        }

        /// <summary>
        /// Forces a full garbage collection.
        /// </summary>
        private static void ForceFullGarbageCollection()
        {
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        }
        
        // ReSharper restore RedundantAssignment
    }
}
